home *** CD-ROM | disk | FTP | other *** search
/ Tech Arsenal 1 / Tech Arsenal (Arsenal Computer).ISO / tek-01 / cuj1008.zip / RAMEY.ZIP / PSORT.NR < prev    next >
Text File  |  1991-09-04  |  24KB  |  560 lines

  1. .sp 15
  2. .ce 3
  3. The Postman's Sort
  4. .sp 1
  5. Robert Ramey
  6. .sp 3
  7. .ce 1
  8. abstract
  9. .sp 1
  10. This article describes a program that sorts an arbitrarily large
  11. number of records in less time than any algorithm based on comparison
  12. sorting can.  For many commonly encountered files, time will be
  13. strictly proportional to the number of records.
  14. It is not a toy program. It can sort on an arbitrary
  15. group of fields with arbitrary collating sequence on each field faster
  16. than any other program available.
  17. .bp
  18. .SH Introduction
  19. .PP
  20. Forget for a moment everything you know about computer science.
  21. .PP
  22. If you were given a stack of playing cards and asked to sort
  23. them by suit, how would you go about it.
  24. .PP
  25. Most people would deal the cards into four piles, one for each
  26. suit.  After dealing the last card the piles would be picked up
  27. one after the other.
  28. Sorting a deck of cards this way into four suits would take
  29. about a minute.
  30. .PP
  31. Suppose that you were given 100 decks of cards into suits.
  32. Would you use
  33. a different method?  How long would it take you to do it.
  34. It should be pretty clear that the same method would be used
  35. by most people and that it would take 100 times as long as for
  36. sorting one deck of cards into four suits.
  37. .PP
  38. So here we have a sorting method which will time strictly
  39. proportional to the number of items to be sorted.  This has
  40. been known as distribution sorting.
  41. .PP
  42. Most computer professionals "know" that time to sort a file
  43. of records must increase disproportionately to the number of records
  44. in the file.  Its a good thing that the rest of us don't know it.
  45. We might never get our mail.
  46. .PP
  47. In fact, most will cite Donald E. Knuth's book
  48. .UL
  49. The Art of Computer Programming:Searching and Sorting
  50. Knuth's book contains a chapter on minimum comparison sorting
  51. which shows that a sorting method which compares records
  52. can do no less than n lg2(n) (note: that's log base 2) comparisons.
  53. However, this analysis
  54. specifically excludes methods that to not use key comparison.
  55. In fact, other sections of the book allude to methods whose time
  56. is proportional to the number of records sorted.
  57. .PP
  58. Then, why does it seem that every sort utility uses quicksort?
  59. Practical sort utilities have to be able to handle a wide
  60. variety of keys and key types and be fast.  The methods described
  61. in Knuth's book that to not use comparison have depended on
  62. the use of fixed length keys.  Also, although they might be
  63. faster for a sufficiently large number of records, they are slow
  64. on the numbers of records usually encountered.
  65. For example, to sort a group of records on
  66. a social security number using radix list sort would require
  67. 30 passes through the file!
  68. .PP
  69. There is a way to overcome these deficiency.  In fact the
  70. post office uses it every day.  While researching this article
  71. (after writing the program), I found that Knuth alluded to it
  72. and left it as an exercise for the reader.
  73. .SH "A Generalized Distribution Sorting Algorithm"
  74. .PP
  75. When a postal clerk recieves a huge bag of letters he distributes
  76. them into other bags by state.  Each bag gets sent to the
  77. indicated state.  Upon arrival, another clerk distributes
  78. the letters in his bag into other bags by city. So the
  79. process continues until the bags are the size one man
  80. can carry and deliver.  This is the basis for my sorting method
  81. which I call the postman's sort.
  82. .PP
  83. Suppose we are given a large list of records to be ordered
  84. alphabetically on a particular field.
  85. Make one pass through the file.  Each record
  86. read is added to one of 26 lists depending on the first
  87. letter in the field.  The first list contains all the
  88. records with fields starting with the letter "A" while the last
  89. contains all
  90. the records with fields starting with the letter "Z".
  91. Now we have divided the
  92. problem down to 26 smaller subproblems.
  93. Now we address subproblem of sorting all the records in the sublist
  94. corresponding to key fields starting with the letter "A".
  95. If there are no records in the "A" sublist we can proceed to
  96. deal with the "B" sublist.
  97. If the "A" sublist contains only one record it can be written
  98. to output and we are done with that sublist.
  99. If the "A" sublist contains more that one record, it must be sorted
  100. then output.  Only when the "A"
  101. list has been disposed of we can move on to each of the other
  102. sublists in sequence.  The records will be written to the output
  103. in alphabetical order.  When the "A" list contains more than
  104. one record it has to be sorted before it is output.  What sorting
  105. algorithm should be used?  Just like a real postman, we use the
  106. postman's sort. Of course we just apply the method to the second
  107. letter of the field.
  108. This is done to greater and greater depths until eventually
  109. all the words starting with "A" are written to the output.
  110. We can then proceed to deal with sublists "B" through "Z"
  111. in the same manner.
  112. .PP
  113. The above algorithm is implemented in the accompanying program.
  114. The function sort() is passed a sublist.  First it is determined
  115. how many records the sublist has.  If its one we're done after
  116. writing the record.  If the sublist is empty we can just return.
  117. Otherwise we advance the pointer into the sort key and continue.
  118. plan() and allocate() reserve memory on the stack
  119. of sublist pointers. dist() distributes the input sublist
  120. among the newly created sublists.  dsort() calls sort()
  121. for each of the newly created sublists.
  122. .SH "Reality Check"
  123. .PP
  124. A practical sort utility must be able to sort on any fields in any sequence
  125. with any collating sequence.  The command line syntax permits
  126. specification of delimited fields, start of key within each
  127. field, and specification of collating sequences for each field.
  128. See the accompanying manual page for the sort program.
  129. Sorting proceeds according to each key field.
  130. The first character within a field which corresponds to a
  131. zero collating sequence terminates the field.  The rest of
  132. the field is not considered in the sort.  Sorting continues
  133. on remaining key fields.
  134. This means that "A<0>C" might be written to output before
  135. "A<0>B" in the final output.
  136. If this behavior is not desired,
  137. one should make sure that every character that could appear
  138. within a field is assigned a non-zero collating sequence value.
  139. .PP
  140. To be really useful the program must take into account that
  141. records can have varying numbers of fields, fields can
  142. have varying number of characters and that the command line
  143. may specify fields and/or key characters that a particular
  144. record does not contain.
  145. In general keys corresponding to non-existant fields are
  146. distributed with a collating sequence of zero.  This
  147. turns out to be more natural, convenient and flexible
  148. of the possibilities considered.
  149. This introduces some complications which require refinements
  150. of the algorithm.  In
  151. .PP
  152. After distribution is completed by dist(). The newly created
  153. sublists are processed by dsort().  Sublists corresponding
  154. to a zero collating value are handled first.
  155. For these sublists, dsort() advances key pointers to the
  156. next key field before continuing to distribute these sublists.
  157. The remaining sublists are handled as previously described.
  158. .SH "Speeding things up"
  159. .PP
  160. Is this the best we can do?  Suppose we wanted to sort 100 decks
  161. of shuffled playing cards by value and suit.  We could first
  162. distribute by suit and then distribute each pile by value.
  163. But we probably wouldn't.  What we would probably do is to
  164. arrange 4 rows and 13 columns.  Each card would be dealt
  165. to the pile corresponding to its suit and value.  All the cards
  166. could be sorted in one pass.
  167. .PP
  168. When we're sorting on the basis of a key field we can do the
  169. same thing by distributing among a larger number of sublists.
  170. In one pass we would have sublists
  171. "\0?", "A\0", "AA", "AB",...,"AZ", "B\0", "BA", ...,"ZY","ZZ".
  172. Of course information on 1 + 26 * (1 + 26) = 703 sublists
  173. must be maintained in memory, but the effect is to reduce the number
  174. of times the key has to be addressed by half.
  175. The basic principle is to
  176. sort on the basis of as many bytes at a time as possible.
  177. If we want to include all 8 bit bytes in the collating sequence
  178. we are going to need 64k sublists to sort on two bytes at a time.
  179. This usually not going to be too practical (at least on Intel type
  180. processors where the maximum size data segment is 64k).
  181. Fortunately, in the
  182. real world we usually know something about the fields we want to sort on.
  183. For example, to create a telephone directory, we sort on peoples
  184. names last name first.  These fields will only contain alphabetic
  185. characters.  Hence for this purpose we can take two bytes at a time.
  186. Social security numbers only contain decimal digits so we can
  187. take 3 or 4 at time before things get out of hand.
  188. .PP
  189. The function plan() called from sort() determines the best number
  190. of bytes to take given the collating sequences defined for the
  191. key fields.  It also sets up pointers to be able to find the key fields.
  192. The program is designed so that we can take bytes from
  193. more than one field on one pass.  This should assure us that the
  194. maximum number of bytes possible has been used to distribute
  195. the record each time it is addresses.
  196. To sort a file of records on a social security number in the
  197. third field in format 999-99-9999 one could use the command line
  198.  
  199. asort -k '0'-'9' -f 2 -c 0 -c 4 -c 7
  200.  
  201. The default setup for asort will allocate sublist space to handle
  202. three decimal digits.  The first time a record is addressed
  203. it will be distributed on the first three digits.
  204. The second time '-' will be encountered at the next key
  205. position so that all the records with the same first three digits
  206. will added to sublist 0.
  207. the third time distribution will be on the middle two digits.  Enough
  208. space will be allocated to distribute on three digits but only
  209. a small portion will be used as each field will be terminated by the '-'.
  210. The fourth distribution will be on the next three digits and the
  211. fifth will be on the last digit and field terminator.
  212. The best command line to sort a file of records based on
  213. a social security number in the third field in format 999-99-9999
  214. would be
  215.  
  216. asort  -k '0'-'9' -f 2 -c 0-2 -c 4-5 -c 7-10 .
  217.  
  218. In this case sort would pass only three times through the records.
  219. In general, the more information we supply in the command
  220. line the faster the sort is likely to be executed.
  221. .SH "Storage Management"
  222. .PP
  223. Sublists are implemented as linked lists of records so that moving
  224. records around is fast and simple. As records are read from
  225. the standard input they are processed by the first
  226. distribution pass.  If all the records don't fit in memory
  227. a temporary disk file containing the sublists is created.
  228. Compiling with the MicroSoft C v5.21 compact memory model(that's
  229. small code/large data),
  230. it turns out that for an average fill with random alphabetic keys, a given
  231. record will be written and read on/from the temporary file
  232. at most once if the
  233. file to be sorted is less than 280MB.  Of course if we're using MSDOS
  234. we run in to other problems first.
  235. .PP
  236. As usual a disproportionate part of the program is taken up
  237. with issues of memory management.  Even so, won't go into it
  238. here as it peripheral to the subject of this article.
  239. .SH "Comparing apples and oranges"
  240. .PP
  241. Below I offer an informal analysis of the speed of the postman's sort
  242. for some different types of files.
  243. Comparative analysis of the comparison sorting and the postman's sort
  244. is somewhat difficult since the methods are so different.  I have chosen
  245. to consider "primitive operations".  By primitive operation I mean
  246. each time a record is addressed and/or manipulated in some way.
  247. If the primitive operation has some sort of inner loop such as
  248. a string comparison does, I count each cycle of the inner loop as
  249. a primitive operation.
  250. .PP
  251. For comparison sorts this will be comparing two bytes
  252. and perhaps moving records.  For the postman's sort
  253. this will be manipulation of a byte from the key field.
  254. Of course, this is going to be a rough
  255. approximation but I think it will still be worthwhile.
  256. .PP
  257. First consider the case of a relatively short fixed length key is
  258. used for the sort.  eg. sorting accounting transactions by date in
  259. format mmdd.
  260. The number of days in a month or year is small enough so that
  261. we would expect to complete the job in one pass through the file
  262. distributing records among 2*10*4*10 = 800 sublists. This is determined
  263. by using a key of command line of
  264.  
  265. -k '0'-`1' -c 0 -k '0'-'9' -c 1 -k '0'-'3' -c 3 -k '0'-'9'
  266.  
  267. In this case the number of primitive operations required by the postman's
  268. sort will be equal to the number of records to be sorted times four operations
  269. per record as there are four bytes in the record.  Note that this is an
  270. example of a sorting method which requires time proportional to the
  271. number of records in the file.  And its not just a toy problem.
  272. .PP
  273. Using any comparison sorting method
  274. the number of comparisons will be at least N*lg2(N).  Since the number of
  275. records far exceeds the number of keys, The number of primitive operations
  276. per comparison will be close to 4.  Using 3 as en estimate the
  277. total number of primitive operations will be about 3*N*lg2(N).
  278. The postman's sort will be faster than the fastest comparison sort
  279. by the following factor:
  280.  
  281. .nf
  282.     3*N*lg2(N)
  283.     --------- = .75*lg2(N)
  284.       4*N
  285. .fi
  286.  
  287. For 100,000 records the fastest comparison sort will require 12 times more
  288. primitive operations than the postman's sort.
  289. .PP
  290. Next consider the case of a relatively long variable length key.
  291. Suppose we use as an example keys made of random alphabetic characters.
  292. Using the postman's sort and taking two characters at a time,
  293. the first pass through the file will generate 703 subfiles.  Each subfile
  294. will in turn be distributed among 703 subfiles, etc.
  295. until each subfile has only
  296. one record.  Hence each record will be addressed approximately
  297. lg703(N) times where N is the number of records in the file.  The total number
  298. of primitive operations will be 2*N*lg703(N).
  299. .PP
  300. Using a comparison sort,
  301. most of the comparisons are between keys that are close together.
  302. If there are less the 26 records in the file we would expect most
  303. comparisons to terminate on the first byte.  If there are less than
  304. 26*26 records in the file most comparisons will terminate on the second
  305. byte and so on.  This works out to lg26(N) primitive operations
  306. for each comparison.  The number of comparisons is at least N*lg2(N).
  307. So the total primitive operation would be N*lg2(N)*lg26(N)
  308. .PP
  309. For this file the postman's sort will be faster than the comparison
  310. sort by the following factor.
  311.  
  312. .nf
  313.     N*lg2(N)*lg26(N)    lg2(N)*ln(703)
  314.     ----------------  =  --------------  =  .9*lg2(N)
  315.     (2*N*lg703(N))         2*ln(26)
  316. .fi
  317.  
  318. or almost 16 times faster for a 100,000 record file.
  319. .SH "Just How Fast Is It"
  320. .PP
  321. The above analysis is rather crude in that it makes a number of
  322. assumptions to make the calculations easier.
  323. The above analysis assumes that all types of "primitive operations"
  324. take the same amount of time to complete.  Also a practical program
  325. spends a lot of time in storage management overhead which the theoretic
  326. approach doesn't take in to account.  Next let's look at some real
  327. results.
  328. In order to test this sorting program, I generated test files consisting
  329. of variable length records of random alphabetic characters.  The average
  330. record length is 15 bytes long.  The files were 1000, 10,000 and 100,000
  331. records long.  (15K, 150K, and 1.5MB long).  For comparison purposes I
  332. used the sort program from the MKS Toolkit from Mortice Kern Systems.
  333. I have assumed that it uses some sort of comparison sort.
  334. The results are summarized
  335. in the following table
  336.  
  337. .nf
  338. .ta
  339. .ta 35R,50R,63R
  340.     Comp. Sort    Postman's Sort    Speed Factor
  341. Time fixed key    3*N*lg2(N)    4*N    .75lg2(N)
  342. Time Variable key    N*lg2(N)*lg26(N)    2*N*lg703(N)    .9*lg2(N)
  343. Sort 1K records    3 sec    3 sec    1.0
  344. Sort 10K records    27sec    18 sec    1.5
  345. Sort 100K records    558sec    290 sec    1.92
  346. .fi
  347.  
  348. My machine is a 12 MegaHertz AT compatible with two 28ms Seagate 251-1
  349. disk drives.  One drive was used for temporary files.  The number of
  350. drives used affected greatly the time for comparison sort of a large
  351. file.  It had little effect on the time required for the postman's
  352. sort.
  353. .PP
  354. Well it looks like the postman's sort isn't as fast my theory predicts.
  355. Buts its still twice as fast at the next best thing.
  356. .SH "Final Observations"
  357. One of the most gratifying things about the postman's sort is that
  358. the first records are produced on the standard output before the
  359. file has even completed reading!  In our test file, records which
  360. contained just a newline character are output as they are read in.
  361. When the last record is read in the first non null records appear
  362. almost immediately.  If one is working with a multi-tasking system
  363. with pipes, much time will be saved as the subsequent process can
  364. proceed parallel with the sorting process.  So even though this
  365. sort is only twice as fast, the jobs that use it may be improved
  366. much more.
  367. .PP
  368. Well, there you have it.  A program which sorts a file of N records
  369. in less time
  370. than it takes to make N*lg2(N) comparisons.  For a large and common
  371. class of sorting keys, it will sort a file in a time proportional the
  372. the number of records in the file.
  373. .bp
  374. NAME
  375.  
  376. psort - sort the standard input file
  377.  
  378. SYNOPSIS
  379.  
  380. psort [-t <dir>] [-s <record size>]
  381. .br
  382.     [ [-k <keys>] ] [ [-f <range>] [-c <range>]... ]...
  383.  
  384. DESCRIPTION
  385.  
  386. psort sorts lines of the standard input file and writes the result
  387. on the standard output.  Default key is the entire line.
  388. Default ordering is lexicographic by bytes in machine
  389. collating sequence.
  390.  
  391. .in +4
  392. .ti -4
  393. -t  use the following name for the temporary directory.
  394. Default is taken from environment variable %TMP%
  395.  
  396. .ti -4
  397. -s  record size.
  398. if a range (eg. 10-124) is specified, records will be of variable
  399. length delimited by a newline.  If a record larger than the indicated
  400. maximum size is encountered the program will terminate with and error
  401. message.
  402. If a single value is specified
  403. records will be fixed in length and the newline will have no special
  404. significance.
  405. If no record size is specified, variable length records upto 511
  406. bytes are assumed.
  407.  
  408. .ti -4
  409. -k  specify the collating sequence for the subsequently specified
  410. sorting fields.  The collating sequence is specified as one
  411. or more ranges of values.  Characters are assigned collating
  412. sequence in order of their specification.  For example,
  413. to sort lower case alphabetic characters use -k 'a'-'z'.
  414. Any characters not specified will be assigned a collating
  415. value of 0.  Characters in a field beyond a character with
  416. a 0 collating value will not be included within the sorted field.
  417. Hence, either of records b<0>a and b<0>c  may precede the other
  418. in the output file.
  419. Within a key specification, any number of ranges may be
  420. specified.  For example, if the sorting field will contain
  421. any combination of lower case letters, digits and spaces,
  422. use -k  ' ' '0'-'9' 'a'-'z'.  Spaces will sort before digits
  423. which will sort before lower case letters.  If no key
  424. collating sequence is specified,  a default collating
  425. sequence of all printable ascii characters is used.
  426.  
  427. .ti -4
  428. -r  repeat previous collating sequence.  For example, to fold
  429. upper case letters to lower case letters for purposes of
  430. determining sorting priority use -k 'a'-'z' -r 'A'-'Z'.
  431. This would assign the first character following the -r
  432. the same collating value as the first one assigned in the
  433. previous range.  To give varying white space characters equal
  434. weight use -k ' ' -r '\t' -r '_' .
  435.  
  436. .ti -4
  437. -n  numeric sort on the key.  This is an alternative to -k.
  438. numeric fields may contain a leading sign and/or decimal point.
  439. Numeric fields should look like
  440. [' ']...[+|-]['0'-'9']...[.]['0'-'9']...
  441. Any non-numeric characters will terminate the field.
  442. The field will be sorted by numeric value.
  443.  
  444. .ti -4
  445. -i  invert the sequence of the sort for the last key specified.
  446.  
  447. .ti -4
  448. -u  output only records that are unique according to the
  449. sorting key fields.
  450.  
  451. .ti -4
  452. -f  sort on one or more fields.  Fields are groups of
  453. characters separated by a delimiter character.
  454. Fields are number starting at 0.  That is -f 0
  455. refers to the start of the record.
  456. A field specification may contain a range of
  457. fields as in -f 2-4 to indicate that sorting
  458. sequence is to be determined on the basis of
  459. the third, fourth and fifth fields in turn.
  460. A range must have a definite end.  ie -f 2-
  461. is not permitted.  A field range need not
  462. be increasing.  ie -f 3-2 is permited and
  463. will sort first by the fourth then by the
  464. third field.
  465.  
  466. .ti -4
  467. -c  sort one or more characters within the indicated
  468. fields.  Start counting character positions from 0.
  469. For example -f 1 -c 2-3 would sort on the third and
  470. fourth characters of the second field.  Several
  471. character ranges may be specified for a given field.
  472. For example -f 2 -c 5-6 -c 3-4 -c 1-2 would specify
  473. three sorting fields of 2 characters each with the
  474. third delimited field.  When specifying a character
  475. range within a field, the second number must be greater
  476. or larger than the first.  ie. -c 7-3 cannot be used.
  477. An indefinite character range can be specified as
  478. in -c 4- . This will indicate all characters starting
  479. with the fifth to the end of the field.
  480.  
  481. .ti -4
  482. <range> a range is used to specify ranges of fields, displacements
  483. within a field and collating values.  The common syntax is
  484. <start>[-[<end>]] . <start> indicates a single value. <start>-
  485. indicates a range beginning at <start> to a large number.  For
  486. example -f 2- would be used to specify all fields after the
  487. second.  <start>-<end> indicates a range of fields.  The
  488. start and end number can be in a number of formats:
  489. simple decimal numbers, numbers starting with 0 are taken
  490. to be octal, numbers starting with 0x are taken to be
  491. hexidecimal and characters within apostrophes are converted
  492. to there ascii value.  Hence -k ' ' 'a'-'z' and -k 0x20 'a'-122
  493. are equivalent.
  494.  
  495. .ti -4
  496. -d  the following character is the field delimiter. For example
  497. -d '|' . The default field delimiter is a tab (0x09).
  498. .in -4
  499.  
  500. If no sorting fields are specified, the whole record is taken as
  501. a sorting field.
  502. Sorting proceeds according to the precedence indicated by
  503. the sequence of the sorting fields.
  504. Records with the same sorting fields
  505. will be output in an unpredictable sequence.
  506.  
  507. Remember that characters not specified within a collating
  508. sequence are taken as collating value zero.
  509. This can result in unexpected behavior when fields
  510. are not the same length. Following is the result of
  511. sorting a small file with -k 'z'-'a'.
  512.  
  513. .nf
  514. def
  515. cad
  516. basdf
  517. a
  518. aa
  519.  
  520. .fi
  521. This was probably not the result intended.  To get the
  522. desired result, use -k 'a'-'z' -i.
  523.  
  524. .nf
  525. def
  526. cad
  527. basdf
  528. aa
  529. a
  530. .fi
  531.  
  532. The following switches normally need not be used.  They are included
  533. for purposes of fine tuning and debugging.
  534.  
  535. .in +4
  536. .ti -4
  537. -m  maximum memory in K to be allocated for sort.  This can be useful
  538. in a multitasking environment so that psort doesn't suck up all the
  539. memory available in the system.
  540.  
  541. .ti -4
  542. -b  specify the size of the buffers used for standard input/output in K.
  543. The default size is 30.
  544.  
  545. .ti -4
  546. -sb specify the size of the output buffer used to write data to
  547. the temporary file. Current value is 30.
  548.  
  549. .ti -4
  550. -rb specify the size of the input buffer used to read data from the
  551. temporary file. Current value is 30.
  552.  
  553. .ti -4
  554. -v  specify visible mode.  This displays statistics on each distribution
  555. pass in the file.  It is useful for debugging and fine tuning.  If only
  556. the top levels of distribution are desired use -v <number of levels>.
  557.  
  558. .ti -4
  559. -l  length of segment used by internal storage in K.  Current value is 16.
  560.